package r25.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;

/**
 * <p>
 * Config2 class.
 * </p>
 * 
 * @author u211354
 * @version $Id: $
 */
public class Config2 implements Map<Object, Object> {
	/** Constant <code>PROPERTIES_TYPE=1</code> */
	public static final int PROPERTIES_TYPE = 1;
	/** Constant <code>JSON_TYPE=2</code> */
	public static final int JSON_TYPE = 2;

	private final Map<Object, Object> config;

	/**
	 * Empty constructor.
	 */
	public Config2() {
		config = new HashMap<Object, Object>();
	}

	/**
	 * Loads a JSON or Properties file from the specified {@code File}.
	 * 
	 * @param file
	 *            a JSON or Properties file
	 * @throws java.io.IOException
	 *             if any
	 */
	public void load(File file) throws IOException {
		if (file != null)
			load(file.toURI().toURL());
	}

	/**
	 * Loads a specified JSON or Properties file from the specified {@code URL}.
	 * 
	 * @param url
	 *            a JSON or Properties file
	 * @throws java.io.IOException
	 *             if any
	 */
	public void load(URL url) throws IOException {
		if (url != null) {
			InputStream in = null;
			try {
				in = url.openStream();
				int type = url.getPath().toLowerCase().endsWith(".json") ? JSON_TYPE
						: PROPERTIES_TYPE;
				load(in, type);
			} finally {
				if (in != null)
					in.close();
			}
		}
	}

	/**
	 * Loads a Properties file from the specified {@code InputStream}.
	 * 
	 * @param in
	 *            a Properties file
	 * @throws java.io.IOException
	 *             if any
	 */
	public void load(InputStream in) throws IOException {
		load(in, PROPERTIES_TYPE);
	}

	/**
	 * Loads a JSON or Properties file from the specified {@code InputStream}
	 * and {@code type} using ISO 8859-1 character encoding.
	 * 
	 * @param in
	 *            a JSON or Properties file
	 * @param type
	 *            {@code PROPERTIES_TYPE} or {@code JSON_TYPE}
	 * @throws java.io.IOException
	 *             if any
	 */
	public void load(InputStream in, int type) throws IOException {
		Reader reader = new InputStreamReader(in, "ISO-8859-1");
		reader = new BufferedReader(reader, 4096);
		load(reader, type);
	}

	/**
	 * Loads a JSON or Properties file from the specified {@code Reader} and
	 * {@code type}.
	 * 
	 * @param in
	 *            a JSON or Properties file
	 * @param type
	 *            {@code PROPERTIES_TYPE} or {@code JSON_TYPE}
	 * @throws java.io.IOException
	 *             if any
	 */
	public void load(Reader in, int type) throws IOException {
		if (type == PROPERTIES_TYPE) {
			Properties props = new Properties();
			props.load(in);
			load(props);
		} else if (type == JSON_TYPE) {
			JsonFactory jsonFactory = new JsonFactory();
			jsonFactory.enable(JsonParser.Feature.ALLOW_COMMENTS);
			JsonParser jsonParser = jsonFactory.createJsonParser(in);

			ObjectMapper jsonMapper = new ObjectMapper();
			Map<String, Object> map = jsonMapper.readValue(jsonParser,
					new TypeReference<Map<String, Object>>() {
					});
			config.putAll(map);
		} else {
			throw new IllegalArgumentException("Unknown type " + type);
		}
	}

	/**
	 * Loads the entries from the specified {@code Map}.
	 * 
	 * @param map
	 *            entries
	 */
	public void load(Map<Object, Object> map) {
		Set<Entry<Object, Object>> set = map.entrySet();
		for (Entry<Object, Object> entry : set)
			setEntry(entry.getKey(), entry.getValue());
	}

	/**
	 * Returns true if this config contains an entry for the specified key.
	 * 
	 * @param key
	 *            a key
	 * @return true if this config contains an entry for the specified key
	 */
	public boolean containsEntry(Object key) {
		if (key == null)
			return false;
		if (key instanceof String)
			return containsEntry(config, (String) key);
		return config.containsKey(key);
	}

	/**
	 * <p>
	 * containsEntry
	 * </p>
	 * 
	 * @param obj
	 *            a {@link java.lang.Object} object.
	 * @param key
	 *            a {@link java.lang.String} object.
	 * @return a boolean.
	 */
	protected boolean containsEntry(Object obj, String key) {
		int j = key.lastIndexOf('.');
		if (j >= 0) {
			obj = getEntry(key.substring(0, j));
			key = key.substring(j + 1);
		}

		try {
			if (obj instanceof Map) {
				Map<?, ?> map = (Map<?, ?>) obj;
				return map.containsKey(key);
			} else if (obj instanceof List) {
				List<?> list = (List<?>) obj;
				int i = Integer.parseInt(key);
				return i >= 0 && i < list.size();
			} else if (obj.getClass().isArray()) {
				int i = Integer.parseInt(key);
				return i >= 0 && i < Array.getLength(obj);
			} else {
				String s = "get" + key.substring(0, 1).toUpperCase()
						+ key.substring(1);
				obj.getClass().getMethod(s, (Class[]) null);
				return true;
			}
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Returns the object to which the specified key is mapped or {@code null}
	 * if this config contains no mapping for the key or if the key is
	 * {@code null}.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Object} object
	 */
	public Object getEntry(Object key) {
		if (key == null)
			return null;
		if (key instanceof String)
			return getEntry(config, (String) key, 0);
		return config.get(key);
	}

	/**
	 * <p>
	 * getEntry
	 * </p>
	 * 
	 * @param obj
	 *            a {@link java.lang.Object} object.
	 * @param key
	 *            a {@link java.lang.String} object.
	 * @param i
	 *            a int.
	 * @return a {@link java.lang.Object} object.
	 */
	protected Object getEntry(Object obj, String key, int i) {
		if (obj == null || key == null || i < 0)
			return obj;

		int j = key.indexOf('.', i);
		String subkey = j < 0 ? key.substring(i) : key.substring(i, j);

		try {
			if (obj instanceof Map) {
				Map<?, ?> map = (Map<?, ?>) obj;
				obj = map.get(subkey);
			} else if (obj instanceof List) {
				List<?> list = (List<?>) obj;
				obj = list.get(Integer.parseInt(subkey));
			} else if (obj.getClass().isArray()) {
				obj = Array.get(obj, Integer.parseInt(subkey));
			} else {
				String s = "get" + subkey.substring(0, 1).toUpperCase()
						+ subkey.substring(1);
				Method m = obj.getClass().getMethod(s, (Class[]) null);
				obj = m.invoke(obj, (Object[]) null);
			}
		} catch (Exception e) {
			return null;
		}

		return getEntry(obj, key, j < 0 ? j : j + 1);
	}

	/**
	 * Associates the specified value with the specified key in this config. If
	 * config map previously contained a mapping for the key, the old value is
	 * replaced.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @param val
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Object} object.
	 */
	public Object setEntry(Object key, Object val) {
		if (val == null)
			return deleteEntry(key);
		if (key instanceof String)
			return setEntry(config, (String) key, 0, val);
		return config.put(key, val);
	}

	/**
	 * <p>
	 * setEntry
	 * </p>
	 * 
	 * @param obj
	 *            a {@link java.lang.Object} object.
	 * @param key
	 *            a {@link java.lang.String} object.
	 * @param i
	 *            a int.
	 * @param val
	 *            a {@link java.lang.Object} object.
	 * @return a {@link java.lang.Object} object.
	 */
	public Object setEntry(Object obj, String key, int i, Object val) {
		if (key == null || i < 0)
			return val;

		int j = key.indexOf('.', i);
		String subkey = j < 0 ? key.substring(i) : key.substring(i, j);

		Object oldVal;
		if (j < 0) {
			if (obj instanceof Map) {
				@SuppressWarnings("unchecked")
				Map<Object, Object> map = (Map<Object, Object>) obj;
				oldVal = map.put(subkey, val);
			} else if (obj instanceof List) {
				@SuppressWarnings("unchecked")
				List<Object> list = (List<Object>) obj;
				i = Integer.parseInt(subkey);
				oldVal = list.set(i, val);
			} else if (obj.getClass().isArray()) {
				i = Integer.parseInt(subkey);
				oldVal = Array.get(obj, i);
				Array.set(obj, i, val);
			} else {
				oldVal = null;
			}
		} else {
			Object newObj = getEntry(obj, subkey, 0);
			if (newObj == null) {
				newObj = new HashMap<Object, Object>();
				@SuppressWarnings("unchecked")
				Map<Object, Object> map = (Map<Object, Object>) obj;
				map.put(subkey, newObj);
			}
			oldVal = setEntry(newObj, key, j < 0 ? j : j + 1, val);
		}
		return oldVal;
	}

	/**
	 * Removes the mapping for the specified key from this config if present.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Object} object.
	 */
	public Object deleteEntry(Object key) {
		if (key == null)
			return null;
		if (key instanceof String)
			return deleteEntry(config, (String) key);
		return config.remove(key);
	}

	/**
	 * <p>
	 * deleteEntry
	 * </p>
	 * 
	 * @param obj
	 *            a {@link java.lang.Object} object.
	 * @param key
	 *            a {@link java.lang.String} object.
	 * @return a {@link java.lang.Object} object.
	 */
	public Object deleteEntry(Object obj, String key) {
		int j = key.lastIndexOf('.');
		if (j >= 0) {
			obj = getEntry(key.substring(0, j));
			key = key.substring(j + 1);
		}

		if (obj instanceof Map) {
			Map<?, ?> map = (Map<?, ?>) obj;
			return map.remove(key);
		} else if (obj instanceof List) {
			List<?> list = (List<?>) obj;
			int i = Integer.parseInt(key);
			return list.remove(i);
		} else {
			return null;
		}
	}

	/**
	 * <p>
	 * dispose
	 * </p>
	 */
	public void dispose() {
		config.clear();
	}

	/**
	 * <p>
	 * filter
	 * </p>
	 * 
	 * @param str
	 *            a {@link java.lang.String} object.
	 * @return a {@link java.lang.String} object.
	 * @throws java.lang.Exception
	 *             if any.
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	protected String filter(String str) throws Exception {
		MessageTextTemplateVelocity mttv = new MessageTextTemplateVelocity();
		return mttv.getMessage(str, (Map) this);
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code String}.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.String} object
	 */
	public String getString(Object key) {
		Object val = getEntry(key);
		if (val != null)
			return val.toString();
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code Boolean}. Returns {@code null} if the value is not a
	 * {@link Boolean}, a {@link String} or a {@link Number}.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Boolean} object
	 */
	public Boolean getBoolean(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Boolean)
				return (Boolean) val;
			if (val instanceof String)
				return Boolean.valueOf((String) val);
			if (val instanceof Number)
				return Boolean.valueOf(((Number) val).longValue() == 0);
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code Integer}. Returns {@code null} if the value is not a
	 * {@link Integer}, a {@link String} or a {@link Number}.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Integer} object
	 */
	public Integer getInteger(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Integer)
				return (Integer) val;
			if (val instanceof String)
				return Integer.valueOf((String) val);
			if (val instanceof Number)
				return Integer.valueOf(((Number) val).intValue());
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code Long}. Returns {@code null} if the value is not a {@link Long}, a
	 * {@link String} or a {@link Number}.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Long} object
	 */
	public Long getLong(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Long)
				return (Long) val;
			if (val instanceof String)
				return Long.valueOf((String) val);
			if (val instanceof Number)
				return Long.valueOf(((Number) val).longValue());
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code Float}. Returns {@code null} if the value is not a {@link Float},
	 * a {@link String} or a {@link Number}.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Float} object
	 */
	public Float getFloat(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Float)
				return (Float) val;
			if (val instanceof String)
				return Float.valueOf((String) val);
			if (val instanceof Number)
				return Float.valueOf(((Number) val).floatValue());
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code Double}. Returns {@code null} if the value is not a {@link Double}
	 * , a {@link String} or a {@link Number}.
	 * 
	 * @param key
	 *            a {@link java.lang.Object} object
	 * @return a {@link java.lang.Double} object
	 */
	public Double getDouble(Object key) {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Double)
				return (Double) val;
			if (val instanceof String)
				return Double.valueOf((String) val);
			if (val instanceof Number)
				return Double.valueOf(((Number) val).doubleValue());
		}
		return null;
	}

	/**
	 * Returns the object to which the specified key is mapped converted to a
	 * {@code IParametrable}. Returns {@code null} if the value is not a
	 * {@link Map} or a {@link String}.
	 * 
	 * @param key
	 *            a {@link Object} object
	 * @return a {@link IParametrable} object
	 * @throws java.lang.Exception
	 *             if an exception occurs
	 */
	public IParametrable getParametrable(Object key) throws Exception {
		Object val = getEntry(key);
		if (val != null) {
			if (val instanceof Map) {
				@SuppressWarnings("unchecked")
				Map<String, Object> map = (Map<String, Object>) val;
				return ParameterHelper2.newInstance(map);
			}
			if (val instanceof String)
				return ParameterHelper2.newInstance((String) val);
		}
		return null;
	}

	/**
	 * <p>
	 * getMap
	 * </p>
	 * 
	 * @return a {@link java.util.Map} object.
	 */
	protected Map<Object, Object> getMap() {
		return config;
	}

	/** {@inheritDoc} */
	@Override
	public int size() {
		return keySet().size();
	}

	/** {@inheritDoc} */
	@Override
	public boolean isEmpty() {
		return config.isEmpty();
	}

	/** {@inheritDoc} */
	@Override
	public boolean containsKey(Object key) {
		return containsEntry(key);
	}

	/** {@inheritDoc} */
	@Override
	public boolean containsValue(Object val) {
		return false;
	}

	/** {@inheritDoc} */
	@Override
	public Object get(Object key) {
		return getEntry(key);
	}

	/** {@inheritDoc} */
	@Override
	public Object put(Object key, Object val) {
		return setEntry(key, val);
	}

	/** {@inheritDoc} */
	@Override
	public Object remove(Object key) {
		return deleteEntry(key);
	}

	/** {@inheritDoc} */
	@Override
	@SuppressWarnings("unchecked")
	public void putAll(Map<? extends Object, ? extends Object> map) {
		load((Map<Object, Object>) map);
	}

	/** {@inheritDoc} */
	@Override
	public void clear() {
		config.clear();
	}

	/** {@inheritDoc} */
	@Override
	public Set<Object> keySet() {
		return Collections.unmodifiableSet(config.keySet());
	}

	/** {@inheritDoc} */
	@Override
	public Collection<Object> values() {
		return Collections.unmodifiableCollection(config.values());
	}

	/** {@inheritDoc} */
	@Override
	public Set<java.util.Map.Entry<Object, Object>> entrySet() {
		return Collections.unmodifiableSet(config.entrySet());
	}

	/**
	 * <p>
	 * main
	 * </p>
	 * 
	 * @param args
	 *            an array of {@link java.lang.String} objects.
	 */
	public static void main(String[] args) {
		Config2 c = new Config2();
		c.setEntry("A.B.C", "BonjourC");
		c.setEntry("A.B.D", "BonjourD");

		System.out.println(c.getEntry("A.B.C"));
		System.out.println(c.getEntry("A.B"));
	}
}
